// Copyright (c) 2023 Presto Labs Pte. Ltd.
// Author: jhkim

#pragma once

#include <cmath>
#include <iomanip>
#include <cxxopts.hpp>
#include <fmt/format.h>
#include <glog/logging.h>
#include <nlohmann/json.hpp>
#include <spdlog/spdlog.h>
#include "backward.hpp"
#include <boost/algorithm/string.hpp>
#include <random>
#include <utility>


inline double norm_pdf(const double& x) {
  return (1.0 / std::sqrt(2.0 * M_PI)) * std::exp(-0.5 * x * x);
}

inline double norm_cdf(const double& x) {
  double k = 1.0 / (1.0 + 0.2316419 * x);
  double k_sum = k * (0.319381530 + k * (-0.356563782 + k * (1.781477937 + k * (-1.821255978 + 1.330274429 * k))));

  if (x >= 0.0) {
    return (1.0 - (1.0 / (std::pow(2 * M_PI, 0.5))) * std::exp(-0.5 * x * x) * k_sum);
  } else {
    return 1.0 - norm_cdf(-x);
  }
}

inline double call_price(const double& S, const double& K, const double& r, const double& T, const double& sigma) {
  double d1 = (std::log(S / K) + (r + (sigma * sigma) / 2.0) * T) / (sigma * std::sqrt(T));
  double d2 = d1 - sigma * std::sqrt(T);
  return S * norm_cdf(d1) - K * std::exp(-r * T) * norm_cdf(d2);
}

inline double put_price(const double& S, const double& K, const double& r, const double& T, const double& sigma) {
  double d1 = (std::log(S / K) + (r + (sigma * sigma) / 2.0) * T) / (sigma * std::sqrt(T));
  double d2 = d1 - sigma * std::sqrt(T);
  return K * std::exp(-r * T) * norm_cdf(-d2) - S * norm_cdf(-d1);
}

inline double call_delta(const double& S, const double& K, const double& r, const double& T, const double& sigma) {
  double d1 = (std::log(S / K) + (r + (sigma * sigma) / 2.0) * T) / (sigma * std::sqrt(T));
  return norm_cdf(d1);
}

inline double normal_distribution_random() {
  static std::mt19937 generator(std::time(0));
  std::normal_distribution<double> distribution(0.0, 1.0);
  return distribution(generator);
}

inline std::pair<double, double> option_price_mc_step(
    const double S,
    const int avg_price_days,
    const double avg_price_start,
    const double atm_multiple,
    const double r,
    const double T,
    const double sigma,
    const int num_simulations) {
  const double step = 1.0 / 365.0;  // assume daily step
  const int avg_price_start_step = static_cast<int>(avg_price_start * 365.0);
  CHECK_LT(avg_price_start_step, 365 * T);
  double payoff_sum = 0.0;
  double strike = 0.0;
  for (int i = 0; i < num_simulations; i++) {
    double ST = S;
    double avg_price{0.0};
    ST *= std::exp(
        (r - 0.5 * sigma * sigma) * (avg_price_start_step * step) +
        sigma * std::sqrt(avg_price_start_step * step) * normal_distribution_random());
    for (int j = avg_price_start_step; j < avg_price_start_step + avg_price_days; j++) {
      ST *= std::exp(
          (r - 0.5 * sigma * sigma) * step +
          sigma * std::sqrt(step) * normal_distribution_random());
      avg_price += ST;
    }
    const int last_step = avg_price_start_step + avg_price_days;
    const double K = avg_price / avg_price_days * atm_multiple;
    ST *= std::exp(
        (r - 0.5 * sigma * sigma) * (T - last_step * step) +
        sigma * std::sqrt(T - last_step * step) * normal_distribution_random());
    double payoff = std::max(ST - K, 0.0);
    payoff_sum += payoff;
    strike += K;
  }
  const double option_price = (payoff_sum / num_simulations);
  const double avg_strike = strike / num_simulations;
  return {option_price, avg_strike};
}

inline double option_price_mc(
    const double S,
    const double K,
    const double r,
    const double T,
    const double sigma,
    const int num_simulations) {
  double payoff_sum = 0.0;
  for (int i = 0; i < num_simulations; i++) {
    double gaussian = normal_distribution_random();
    double ST = S * std::exp((r - 0.5 * sigma * sigma) * T + sigma * std::sqrt(T) * gaussian);
    double payoff = std::max(ST - K, 0.0);
    payoff_sum += payoff;
  }
  double option_price = (payoff_sum / num_simulations);
  return option_price;
}

inline void InitializeTracer() {
  ::std::set_terminate([] {
    ::backward::StackTrace st;
    st.load_here(32);
    ::backward::Printer p;
    p.object = true;
    p.color_mode = ::backward::ColorMode::always;
    p.address = true;
    p.print(st, stderr);
    ::std::abort();
  });
}
